// Copyright (c) HashiCorp, Inc
// SPDX-License-Identifier: MPL-2.0
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { Language, TerraformModuleConstraint } from "@cdktf/commons";
import { ConstructsMaker } from "../../constructs-maker";
import { expectModuleToMatchSnapshot } from "../util";
import { execSync } from "child_process";

const onTf1_6AndNewer = (
  name: string,
  fn: () => Promise<void>,
  timeout?: number,
) => {
  const terraformBinaryName = process.env.TERRAFORM_BINARY_NAME || "terraform";
  const output = execSync(`${terraformBinaryName} version -json`);

  if (!output) {
    throw new Error("Could not determine Terraform version");
  }

  const tfVersion = JSON.parse(output.toString()).terraform_version;

  const [major, minor] = tfVersion.toString().split(".");
  if (Number(major) < 1 || (Number(major) === 1 && Number(minor) < 6)) {
    test.skip(
      `${name} (requires Terraform >= 1.6.0; but is ${tfVersion})`,
      fn,
      timeout,
    );
  } else {
    test(name, fn, timeout);
  }
};

test("generate some modules", async () => {
  const workdir = fs.mkdtempSync(
    path.join(os.tmpdir(), "module-generator.test"),
  );
  const constraint = new TerraformModuleConstraint(
    "terraform-aws-modules/eks/aws@7.0.1",
  );

  const maker = new ConstructsMaker(
    {
      codeMakerOutput: workdir,
      targetLanguage: Language.TYPESCRIPT,
    },
    process.env.CDKTF_EXPERIMENTAL_PROVIDER_SCHEMA_CACHE_PATH,
  );
  await maker.generate([constraint]);

  const output = fs.readFileSync(
    path.join(workdir, "modules/terraform-aws-modules/aws/eks.ts"),
    "utf-8",
  );
  expect(output).toMatchSnapshot();
}, 120000);

expectModuleToMatchSnapshot("no module outputs", "generator", [
  "module-no-outputs.test.fixture.tf",
]);

expectModuleToMatchSnapshot("typeless variables", "generator", [
  "module-no-variable-type.test.fixture.tf",
]);

expectModuleToMatchSnapshot("no newline", "generator", [
  "module-no-newline-1.test.fixture.tf",
  "module-no-newline-2.test.fixture.tf",
]);

test("generate multiple aws modules", async () => {
  jest.setTimeout(120000);

  const workdir = fs.mkdtempSync(
    path.join(os.tmpdir(), "module-generator-aws.test"),
  );
  const constraints = [
    new TerraformModuleConstraint("terraform-aws-modules/vpc/aws@2.78.0"),
    new TerraformModuleConstraint("terraform-aws-modules/rds-aurora/aws@4.1.0"),
  ];

  const maker = new ConstructsMaker(
    {
      codeMakerOutput: workdir,
      targetLanguage: Language.TYPESCRIPT,
    },
    process.env.CDKTF_EXPERIMENTAL_PROVIDER_SCHEMA_CACHE_PATH,
  );
  await maker.generate(constraints);

  const vpcOutput = fs.readFileSync(
    path.join(workdir, "modules/terraform-aws-modules/aws/vpc.ts"),
    "utf-8",
  );
  expect(vpcOutput).toMatchSnapshot();

  const rdsOutput = fs.readFileSync(
    path.join(workdir, "modules/terraform-aws-modules/aws/rds-aurora.ts"),
    "utf-8",
  );
  expect(rdsOutput).toMatchSnapshot();
}, 120000);

test("generate nested module", async () => {
  jest.setTimeout(120000);

  const workdir = fs.mkdtempSync(
    path.join(os.tmpdir(), "module-generator-nested.test"),
  );
  const constraint = new TerraformModuleConstraint(
    "terraform-aws-modules/vpc/aws//modules/vpc-endpoints@3.19.0",
  );

  const maker = new ConstructsMaker(
    {
      codeMakerOutput: workdir,
      targetLanguage: Language.TYPESCRIPT,
    },
    process.env.CDKTF_EXPERIMENTAL_PROVIDER_SCHEMA_CACHE_PATH,
  );
  await maker.generate([constraint]);

  const output = fs.readFileSync(
    path.join(
      workdir,
      "modules/terraform-aws-modules/aws/vpc/modules/vpc-endpoints.ts",
    ),
    "utf-8",
  );
  expect(output).toMatchSnapshot();
});

expectModuleToMatchSnapshot("getX variables", "generator", [
  "module-get-x.test.fixture.tf",
]);

expectModuleToMatchSnapshot(
  "handle */* in module variable default string",
  "generator",
  ["module-with-star-default.test.fixture.tf"],
);

onTf1_6AndNewer(
  "generate module that can't be initialized",
  async () => {
    jest.setTimeout(120000);

    const workdir = fs.mkdtempSync(
      path.join(os.tmpdir(), "module-generator.test-no-init"),
    );
    const constraint = new TerraformModuleConstraint(
      "milliHQ/next-js/aws@1.0.0-canary.5",
    );

    const maker = new ConstructsMaker(
      {
        codeMakerOutput: workdir,
        targetLanguage: Language.TYPESCRIPT,
      },
      process.env.CDKTF_EXPERIMENTAL_PROVIDER_SCHEMA_CACHE_PATH,
    );
    await maker.generate([constraint]);

    const output = fs.readFileSync(
      path.join(workdir, "modules/milliHQ/aws/next-js.ts"),
      "utf-8",
    );

    // yes, this is a lot of code, but this test is skipped for some Terraform versions
    // which then shows snapshots as obsolete which fails the test
    // this way, there's no such problem if the test is skipped
    expect(output).toMatchInlineSnapshot(`
      "// generated by cdktf get
      // milliHQ/next-js/aws
      import { TerraformModule, TerraformModuleUserConfig } from 'cdktf';
      import { Construct } from 'constructs';
      export interface NextJsConfig extends TerraformModuleUserConfig {
        /**
        * ACM certificate arn for custom_domain
        */
        readonly cloudfrontAcmCertificateArn?: string;
        /**
        * Aliases for custom_domain
        *
        */
        readonly cloudfrontAliases?: string[];
        /**
        * Header keys that should be used to calculate the cache key in CloudFront.
        * Authorization
        */
        readonly cloudfrontCacheKeyHeaders?: string[];
        /**
        * Controls whether the main CloudFront distribution should be created.
        * true
        */
        readonly cloudfrontCreateDistribution?: boolean;
        /**
        * When using an external CloudFront distribution provide its arn.
        */
        readonly cloudfrontExternalArn?: string;
        /**
        * When using an external CloudFront distribution provide its id.
        */
        readonly cloudfrontExternalId?: string;
        /**
        * The minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections. One of SSLv3, TLSv1, TLSv1_2016, TLSv1.1_2016, TLSv1.2_2018 or TLSv1.2_2019.
        * TLSv1
        */
        readonly cloudfrontMinimumProtocolVersion?: string;
        /**
        * Id of a custom request policy that overrides the default policy (AllViewer). Can be custom or managed.
        */
        readonly cloudfrontOriginRequestPolicy?: string;
        /**
        * Price class for the CloudFront distributions (main & proxy config). One of PriceClass_All, PriceClass_200, PriceClass_100.
        * PriceClass_100
        */
        readonly cloudfrontPriceClass?: string;
        /**
        * Id of a response headers policy. Can be custom or managed. Default is empty.
        */
        readonly cloudfrontResponseHeadersPolicy?: string;
        /**
        * An optional webacl2 arn or webacl id to associate with the cloudfront distribution
        */
        readonly cloudfrontWebaclId?: string;
        /**
        * Controls whether resources for image optimization support should be created or not.
        * true
        */
        readonly createImageOptimization?: boolean;
        /**
        * Use locally built packages rather than download them from npm.
        */
        readonly debugUseLocalPackages?: boolean;
        /**
        * Identifier for the deployment group (only lowercase alphanumeric characters and hyphens are allowed).
        * tf-next
        */
        readonly deploymentName?: string;
        /**
        * Controls whether it should be possible to run multiple deployments in parallel (requires multiple_deployments_base_domain).
        */
        readonly enableMultipleDeployments?: boolean;
        /**
        * Amount of memory in MB the worker Lambda Function for image optimization can use. Valid value between 128 MB to 10,240 MB, in 1 MB increments.
        * 2,048
        */
        readonly imageOptimizationLambdaMemorySize?: number;
        /**
        * Whether to deploy additional lambda JSON policies. If false, lambda_policy_json will not be attached to the lambda function. (Necessary since policy strings are only known after apply when using Terraforms data.aws_iam_policy_document)
        */
        readonly lambdaAttachPolicyJson?: boolean;
        /**
        * Set to true if the Lambda functions should be attached to a VPC. Use this setting if VPC resources should be accessed by the Lambda functions. When setting this to true, use vpc_security_group_ids and vpc_subnet_ids to specify the VPC networking. Note that attaching to a VPC would introduce a delay on to cold starts
        */
        readonly lambdaAttachToVpc?: boolean;
        /**
        * Additional policy document as JSON to attach to the Lambda Function role
        */
        readonly lambdaPolicyJson?: string;
        /**
        * ARN of IAM policy that scopes aws_iam_role access for the lambda
        */
        readonly lambdaRolePermissionsBoundary?: string;
        /**
        * Default wildcard domain where new deployments should be available. Should be in the form of *.example.com.
        */
        readonly multipleDeploymentsBaseDomain?: string;
        /**
        * Tag metadata to label AWS resources that support tags.
        * The property type contains a map, they have special handling, please see {@link cdk.tf/module-map-inputs the docs}
        */
        readonly tags?: { [key: string]: string };
        /**
        * Tag metadata to label AWS S3 buckets. Overrides tags with the same name in input variable tags.
        * The property type contains a map, they have special handling, please see {@link cdk.tf/module-map-inputs the docs}
        */
        readonly tagsS3Bucket?: { [key: string]: string };
        /**
        * The list of Security Group IDs to be used by the Lambda functions. lambda_attach_to_vpc should be set to true for these to be applied.
        *
        */
        readonly vpcSecurityGroupIds?: string[];
        /**
        * The list of VPC subnet IDs to attach the Lambda functions. lambda_attach_to_vpc should be set to true for these to be applied.
        *
        */
        readonly vpcSubnetIds?: string[];
      }
      /**
      * Defines an NextJs based on a Terraform module
      *
      * Docs at Terraform Registry: {@link https://registry.terraform.io/modules/milliHQ/next-js/aws/1.0.0-canary.5 milliHQ/next-js/aws}
      */
      export class NextJs extends TerraformModule {
        private readonly inputs: { [name: string]: any } = { }
        public constructor(scope: Construct, id: string, config: NextJsConfig = {}) {
          super(scope, id, {
            ...config,
            source: 'milliHQ/next-js/aws',
            version: '1.0.0-canary.5',
          });
          this.cloudfrontAcmCertificateArn = config.cloudfrontAcmCertificateArn;
          this.cloudfrontAliases = config.cloudfrontAliases;
          this.cloudfrontCacheKeyHeaders = config.cloudfrontCacheKeyHeaders;
          this.cloudfrontCreateDistribution = config.cloudfrontCreateDistribution;
          this.cloudfrontExternalArn = config.cloudfrontExternalArn;
          this.cloudfrontExternalId = config.cloudfrontExternalId;
          this.cloudfrontMinimumProtocolVersion = config.cloudfrontMinimumProtocolVersion;
          this.cloudfrontOriginRequestPolicy = config.cloudfrontOriginRequestPolicy;
          this.cloudfrontPriceClass = config.cloudfrontPriceClass;
          this.cloudfrontResponseHeadersPolicy = config.cloudfrontResponseHeadersPolicy;
          this.cloudfrontWebaclId = config.cloudfrontWebaclId;
          this.createImageOptimization = config.createImageOptimization;
          this.debugUseLocalPackages = config.debugUseLocalPackages;
          this.deploymentName = config.deploymentName;
          this.enableMultipleDeployments = config.enableMultipleDeployments;
          this.imageOptimizationLambdaMemorySize = config.imageOptimizationLambdaMemorySize;
          this.lambdaAttachPolicyJson = config.lambdaAttachPolicyJson;
          this.lambdaAttachToVpc = config.lambdaAttachToVpc;
          this.lambdaPolicyJson = config.lambdaPolicyJson;
          this.lambdaRolePermissionsBoundary = config.lambdaRolePermissionsBoundary;
          this.multipleDeploymentsBaseDomain = config.multipleDeploymentsBaseDomain;
          this.tags = config.tags;
          this.tagsS3Bucket = config.tagsS3Bucket;
          this.vpcSecurityGroupIds = config.vpcSecurityGroupIds;
          this.vpcSubnetIds = config.vpcSubnetIds;
        }
        public get cloudfrontAcmCertificateArn(): string | undefined {
          return this.inputs['cloudfront_acm_certificate_arn'] as string | undefined;
        }
        public set cloudfrontAcmCertificateArn(value: string | undefined) {
          this.inputs['cloudfront_acm_certificate_arn'] = value;
        }
        public get cloudfrontAliases(): string[] | undefined {
          return this.inputs['cloudfront_aliases'] as string[] | undefined;
        }
        public set cloudfrontAliases(value: string[] | undefined) {
          this.inputs['cloudfront_aliases'] = value;
        }
        public get cloudfrontCacheKeyHeaders(): string[] | undefined {
          return this.inputs['cloudfront_cache_key_headers'] as string[] | undefined;
        }
        public set cloudfrontCacheKeyHeaders(value: string[] | undefined) {
          this.inputs['cloudfront_cache_key_headers'] = value;
        }
        public get cloudfrontCreateDistribution(): boolean | undefined {
          return this.inputs['cloudfront_create_distribution'] as boolean | undefined;
        }
        public set cloudfrontCreateDistribution(value: boolean | undefined) {
          this.inputs['cloudfront_create_distribution'] = value;
        }
        public get cloudfrontExternalArn(): string | undefined {
          return this.inputs['cloudfront_external_arn'] as string | undefined;
        }
        public set cloudfrontExternalArn(value: string | undefined) {
          this.inputs['cloudfront_external_arn'] = value;
        }
        public get cloudfrontExternalId(): string | undefined {
          return this.inputs['cloudfront_external_id'] as string | undefined;
        }
        public set cloudfrontExternalId(value: string | undefined) {
          this.inputs['cloudfront_external_id'] = value;
        }
        public get cloudfrontMinimumProtocolVersion(): string | undefined {
          return this.inputs['cloudfront_minimum_protocol_version'] as string | undefined;
        }
        public set cloudfrontMinimumProtocolVersion(value: string | undefined) {
          this.inputs['cloudfront_minimum_protocol_version'] = value;
        }
        public get cloudfrontOriginRequestPolicy(): string | undefined {
          return this.inputs['cloudfront_origin_request_policy'] as string | undefined;
        }
        public set cloudfrontOriginRequestPolicy(value: string | undefined) {
          this.inputs['cloudfront_origin_request_policy'] = value;
        }
        public get cloudfrontPriceClass(): string | undefined {
          return this.inputs['cloudfront_price_class'] as string | undefined;
        }
        public set cloudfrontPriceClass(value: string | undefined) {
          this.inputs['cloudfront_price_class'] = value;
        }
        public get cloudfrontResponseHeadersPolicy(): string | undefined {
          return this.inputs['cloudfront_response_headers_policy'] as string | undefined;
        }
        public set cloudfrontResponseHeadersPolicy(value: string | undefined) {
          this.inputs['cloudfront_response_headers_policy'] = value;
        }
        public get cloudfrontWebaclId(): string | undefined {
          return this.inputs['cloudfront_webacl_id'] as string | undefined;
        }
        public set cloudfrontWebaclId(value: string | undefined) {
          this.inputs['cloudfront_webacl_id'] = value;
        }
        public get createImageOptimization(): boolean | undefined {
          return this.inputs['create_image_optimization'] as boolean | undefined;
        }
        public set createImageOptimization(value: boolean | undefined) {
          this.inputs['create_image_optimization'] = value;
        }
        public get debugUseLocalPackages(): boolean | undefined {
          return this.inputs['debug_use_local_packages'] as boolean | undefined;
        }
        public set debugUseLocalPackages(value: boolean | undefined) {
          this.inputs['debug_use_local_packages'] = value;
        }
        public get deploymentName(): string | undefined {
          return this.inputs['deployment_name'] as string | undefined;
        }
        public set deploymentName(value: string | undefined) {
          this.inputs['deployment_name'] = value;
        }
        public get enableMultipleDeployments(): boolean | undefined {
          return this.inputs['enable_multiple_deployments'] as boolean | undefined;
        }
        public set enableMultipleDeployments(value: boolean | undefined) {
          this.inputs['enable_multiple_deployments'] = value;
        }
        public get imageOptimizationLambdaMemorySize(): number | undefined {
          return this.inputs['image_optimization_lambda_memory_size'] as number | undefined;
        }
        public set imageOptimizationLambdaMemorySize(value: number | undefined) {
          this.inputs['image_optimization_lambda_memory_size'] = value;
        }
        public get lambdaAttachPolicyJson(): boolean | undefined {
          return this.inputs['lambda_attach_policy_json'] as boolean | undefined;
        }
        public set lambdaAttachPolicyJson(value: boolean | undefined) {
          this.inputs['lambda_attach_policy_json'] = value;
        }
        public get lambdaAttachToVpc(): boolean | undefined {
          return this.inputs['lambda_attach_to_vpc'] as boolean | undefined;
        }
        public set lambdaAttachToVpc(value: boolean | undefined) {
          this.inputs['lambda_attach_to_vpc'] = value;
        }
        public get lambdaPolicyJson(): string | undefined {
          return this.inputs['lambda_policy_json'] as string | undefined;
        }
        public set lambdaPolicyJson(value: string | undefined) {
          this.inputs['lambda_policy_json'] = value;
        }
        public get lambdaRolePermissionsBoundary(): string | undefined {
          return this.inputs['lambda_role_permissions_boundary'] as string | undefined;
        }
        public set lambdaRolePermissionsBoundary(value: string | undefined) {
          this.inputs['lambda_role_permissions_boundary'] = value;
        }
        public get multipleDeploymentsBaseDomain(): string | undefined {
          return this.inputs['multiple_deployments_base_domain'] as string | undefined;
        }
        public set multipleDeploymentsBaseDomain(value: string | undefined) {
          this.inputs['multiple_deployments_base_domain'] = value;
        }
        public get tags(): { [key: string]: string } | undefined {
          return this.inputs['tags'] as { [key: string]: string } | undefined;
        }
        public set tags(value: { [key: string]: string } | undefined) {
          this.inputs['tags'] = value;
        }
        public get tagsS3Bucket(): { [key: string]: string } | undefined {
          return this.inputs['tags_s3_bucket'] as { [key: string]: string } | undefined;
        }
        public set tagsS3Bucket(value: { [key: string]: string } | undefined) {
          this.inputs['tags_s3_bucket'] = value;
        }
        public get vpcSecurityGroupIds(): string[] | undefined {
          return this.inputs['vpc_security_group_ids'] as string[] | undefined;
        }
        public set vpcSecurityGroupIds(value: string[] | undefined) {
          this.inputs['vpc_security_group_ids'] = value;
        }
        public get vpcSubnetIds(): string[] | undefined {
          return this.inputs['vpc_subnet_ids'] as string[] | undefined;
        }
        public set vpcSubnetIds(value: string[] | undefined) {
          this.inputs['vpc_subnet_ids'] = value;
        }
        public get apiEndpointOutput() {
          return this.getString('api_endpoint')
        }
        public get apiEndpointAccessPolicyArnOutput() {
          return this.getString('api_endpoint_access_policy_arn')
        }
        public get cloudfrontCustomErrorResponseOutput() {
          return this.getString('cloudfront_custom_error_response')
        }
        public get cloudfrontDefaultCacheBehaviorOutput() {
          return this.getString('cloudfront_default_cache_behavior')
        }
        public get cloudfrontDefaultRootObjectOutput() {
          return this.getString('cloudfront_default_root_object')
        }
        public get cloudfrontDomainNameOutput() {
          return this.getString('cloudfront_domain_name')
        }
        public get cloudfrontHostedZoneIdOutput() {
          return this.getString('cloudfront_hosted_zone_id')
        }
        public get cloudfrontOrderedCacheBehaviorsOutput() {
          return this.getString('cloudfront_ordered_cache_behaviors')
        }
        public get cloudfrontOriginsOutput() {
          return this.getString('cloudfront_origins')
        }
        public get uploadBucketIdOutput() {
          return this.getString('upload_bucket_id')
        }
        protected synthesizeAttributes() {
          return this.inputs;
        }
        protected synthesizeHclAttributes(): { [name: string]: any } {
          return Object.fromEntries(
            Object.entries(this.inputs)
              .filter(([, val]) => val !== undefined)
              .map(([key, val]) => {
                return [
                  key,
                  {
                    value: val,
                    type: "any",
                  },
                ];
              })
          );
        }
      }
      "
    `);
  },
  120000,
);
